//
//  webserver.c
//
//  Created by Alexander Polyakov on 11/10/24
//
//  Copyright (c) 2024 Trident IOT
//
//
#include <stdio.h>
#include <string.h>
#include <sys/param.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include <dirent.h>

#include "ZMalloc.h"
#include "ZWayLib.h"
#include "ZLogging.h"
#include "ZSerializeZWayParseCommandsPrivate.h"
#include "ZSerializeCommonParsePrivate.h"
#include "ZWayEsp32WifiPrivate.h"
#include "ZWayEsp32WebserverPrivate.h"
#include "ZUrlPrivate.h"
#include "ZFileOperationsPrivate.h"
#include "ZDefsPrivate.h"
#include "CommandClassesPrivate.h"
#include "ZByteOperationsPrivate.h"
#include "ZSerializeZWayEncryptionKeysPrivate.h"
#include "ZSerializeZWayBackupRestorePrivate.h"
#include "ZStreamPrivate.h"
#include "ZController.h"
#include "ZWayPrivate.h"
#include "ZDDXPrivate.h"

#include "esp_http_client.h"
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
#include "esp_crt_bundle.h"
#endif
#include "esp_err.h"
#include "esp_log.h"
#include "esp_vfs.h"
#include "esp_spiffs.h"
#include "esp_littlefs.h"

#define ZWAY_ESP32_WEB_MAX_AGE "86400"

#ifndef ZWAY_ESP32_WEB_SERVER_STACK
#define ZWAY_ESP32_WEB_SERVER_STACK 6000
#endif

static const ZWCHAR __web_arch_translations_name[] = "translations";
static const ZWCHAR __web_arch_name[] = "expert";
static const size_t __web_arch_name_length = (sizeof(__web_arch_name) - sizeof(__web_arch_name[0]));
static const ZWCHAR __web_arch_name_index_html[] = "expert/index.html";

#define ZWAY_ESP32_MIN_SPACE_STORAGE (500 * 1024)
#define ZWAY_ESP32_MAX_FILE_SIZE (700 * 1024)

typedef struct _ZwayEspWebServerData_s
{
    // Z-Way object pointer
    ZWay zway;
} _ZwayEspWebServerData_t;

typedef struct __HttpHandlerArgs_s
{
    httpd_req_t *req;
    ZWBOOL b_begin;
} __HttpHandlerArgs_t;

typedef struct __Esp32HandlerCtxCmd_s
{
    _ZStreamHttpWriteEspCtx_t ctx_stream;
    __HttpHandlerArgs_t args;
    _ZSerializeCommonSettings_t option;
    _ZSerializeZWayParseCommandReg m;
    ZWBYTE buffer[ZWAY_ESP32_WEB_SCRATCH_BUFSIZE];
} __Esp32HandlerCtxCmd_t;

typedef struct __Esp32HandlerCtxEmbed_s
{
    _ZSerializeZWayParseCommandReg m;
    _ZSerializeCommonParseArgumentsContainer_t container[2];
    _ZwayEsp32WifiCredentials_t cred;
} __Esp32HandlerCtxEmbed_t;

typedef struct __Esp32HandlerCtxTar_s
{
    ZWCHAR buffer[128];
    _ZFileOperationsTarDataOutput_t tar_data_output;
} __Esp32HandlerCtxTar_t;

typedef struct __Esp32HandlerCtxUpdate_s
{
    _ZStreamStoragUpdateResult_t result;
    _ZStreamStorageUpdateRwCtx_t ctx_storage;
    ZWNODE node_id;
} __Esp32HandlerCtxUpdate_t;

typedef struct __Esp32HandlerCtxRestore_s
{
    _ZStreamStoragRestoreResult_t result;
    _ZStreamStorageRestoreRwCtx_t ctx_storage;
    ZWBOOL force;
} __Esp32HandlerCtxRestore_t;

typedef enum __Esp32HandlerCtxDownloadType_e
{
    __Esp32HandlerCtxDownloadTypeUpdate,
    __Esp32HandlerCtxDownloadTypeRestore,
} __Esp32HandlerCtxDownloadType_t;

typedef struct __Esp32HandlerCtxDownload_s
{
    union
    {
        __Esp32HandlerCtxUpdate_t update;
        __Esp32HandlerCtxRestore_t restore;
    };
    _ZSerializeDownloadCtx_t ctx_serialize;
    __Esp32HandlerCtxDownloadType_t type;
    ZWCHAR buffer[Z_PLATFORM_MEMORY_BLOCK_SIZE];
} __Esp32HandlerCtxDownload_t;

typedef struct __Esp32HandlerCtx_s
{
    union
    {
        ZWCHAR accept_encoding[Z_STATIC_STRLEN("Accept-Encoding: deflate;q=1.0, gzip;q=1.0, gzip;q=1.0, be;q=1.0, zstd;q=1.0, dcb;q=1.0, dcz;q=1.0, identity;q=1.0 compress;q=1.0, *;q=0.5")];
        __Esp32HandlerCtxCmd_t cgi;
        __Esp32HandlerCtxEmbed_t embed;
        __Esp32HandlerCtxTar_t tar;
        __Esp32HandlerCtxDownload_t download;
        __Esp32HandlerCtxUpdate_t update;
    } common;
} __Esp32HandlerCtx_t;

typedef struct __Esp32ExtensionToContent_s
{
    ZWCSTR extension;
    ZWCSTR content_type;
} __Esp32ExtensionToContent_t;

static ZWError ___httpd_handler_cmd_parser_done(httpd_req_t *req)
{
    ZWError err;

    err = NoError;
    if (httpd_resp_set_type(req, "application/json") != ESP_OK)
    {
        err = BadData;
    }
    if (httpd_resp_sendstr(req, "{\"DONE\":true}") != ESP_OK)
    {
        err = BadData;
    }
    return err;
}

static ZWError __httpd_resp_send_err(httpd_req_t *req, httpd_err_code_t error, ZWCSTR msg, ZWError err)
{
    if (httpd_resp_send_err(req, error, msg) != ESP_OK)
    {
        return BadData;
    }
    return err;
}

static ZWError __httpd_handler_cmd_parser_embed(httpd_req_t *const req, const _ZwayEspWebServerData_t *const data, __Esp32HandlerCtx_t *const alloc, const ZWSTR cmd)
{
    if (_zserialize_zway_parse_command_match(cmd, ZSERIALIZE_ZWAY_PARSE_REG_ZWAY_FUNCTION, alloc->common.embed.m))
    {
        const ZWCSTR func_name = _zserialize_zway_reg_extract_command_path(alloc->common.embed.m, cmd);
        const ZWCSTR param_lists = _zserialize_zway_reg_extract_path_arg(alloc->common.embed.m, cmd);

        if (strcmp(func_name, "setWiFiCredentials") == 0)
        {
            static const _z_serialize_commonParse_arguments_type_t type[] = {_ZSerializeCommonParseArgumentsTypeString, _ZSerializeCommonParseArgumentsTypeString};
            const ZWError err = _zserialize_common_parse_arguments_packs(zway_get_logger(data->zway), param_lists, &alloc->common.embed.container[0], &type[0], (sizeof(type) / sizeof(type[0])));
            if (err != NoError)
            {
                return err;
            }
            (void)strcpy(alloc->common.embed.cred.info.ssid, alloc->common.embed.container[0].value.strValue);
            (void)strcpy(alloc->common.embed.cred.info.psw, alloc->common.embed.container[1].value.strValue);
            _zway_esp32_save_wifi_credentials(ZWAY_ESP32_WIFI_CREDENTIAL_FILENAME, &alloc->common.embed.cred);
            _zserialize_common_parse_arguments_free_containers(&alloc->common.embed.container[0], (sizeof(type) / sizeof(type[0])));
            return ___httpd_handler_cmd_parser_done(req);
        }

        if (strcmp(func_name, "reboot") == 0)
        {
            (void)___httpd_handler_cmd_parser_done(req);
            esp_restart();
            return NoError;
        }
    }
    return __httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, NULL, NotSupported);
}

static size_t __get_hdr_value_str(void *hdr_req, ZWSTR dst, size_t dst_size, _ZSerializeDownloadHdrField_t field)
{
    ZWCSTR field_str;

    switch (field)
    {
        case _ZSerializeDownloadHdrFieldContentLength:
            field_str = "Content-Length";
            break;
        case _ZSerializeDownloadHdrFieldContentType:
            field_str = "Content-Type";
            break;
        default:
            return 0;
    }
    if (httpd_req_get_hdr_value_str((httpd_req_t *)hdr_req, field_str, dst, dst_size) != ESP_OK)
    {
        return 0;
    }
    return strlen(dst);
}

static void __zwaysrv_upgrade_deinit_all(__Esp32HandlerCtxDownload_t *const ctx_download)
{
    _zserialize_download_deinit(&ctx_download->ctx_serialize);
    switch (ctx_download->type)
    {
        case __Esp32HandlerCtxDownloadTypeUpdate:
            _zstream_storage_update_rw_free(&ctx_download->update.ctx_storage);
            break;
        case __Esp32HandlerCtxDownloadTypeRestore:
            _zstream_storage_restore_rw_free(&ctx_download->restore.ctx_storage);
            break;
    }
}

static ZWError __zwaysrv_upgrade_deinit_all_err(__Esp32HandlerCtxDownload_t *const ctx_download, httpd_req_t *const req, const httpd_err_code_t error, const ZWCSTR msg, const ZWError err)
{
    __zwaysrv_upgrade_deinit_all(ctx_download);
    return __httpd_resp_send_err(req, error, msg, err);
}

static ZWError __download_file_sub(__Esp32HandlerCtxDownload_t *const ctx_download, const ZWay zway, const ZWCSTR url, const esp_http_client_handle_t client, const _ZStreamRW_t *const stream, void *const ctx_stream)
{
    esp_err_t status = esp_http_client_set_url(client, url);
    if (status != ESP_OK)
    {
        return InternalError;
    }
    status = esp_http_client_set_method(client, HTTP_METHOD_GET);
    if (status != ESP_OK)
    {
        return InvalidArg;
    }
    status = esp_http_client_open(client, 0);
    if (status != ESP_OK)
    {
        return InternalError;
    }
    ssize_t content_length = esp_http_client_fetch_headers(client);
    if (content_length <= 0)
    {
        return BadData;
    }
    ZWError err = stream->writer.allocate(ctx_stream, content_length);
    if (err != NoError)
    {
        return err;
    }
    while (content_length > 0)
    {
        const ssize_t read_len = esp_http_client_read(client, ctx_download->buffer, sizeof(ctx_download->buffer));
        if (read_len <= 0)
        {
            return BadData;
        }
        err = stream->writer.write(ctx_stream, ctx_download->buffer, read_len);
        if (err != NoError)
        {
            return err;
        }
        content_length = content_length - read_len;
    }
    return NoError;
}

static ZWError __download_file(__Esp32HandlerCtxDownload_t *const ctx_download, const ZWay zway, const ZWCSTR url, const _ZStreamRW_t *const stream, void *const ctx_stream)
{
    static const esp_http_client_config_t config =
        {
            .url = "http://127.0.0.1/", // Just any valid URL, if we leave it empty, an exception will occur
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
            .crt_bundle_attach = esp_crt_bundle_attach // https support
#endif
        };

    const esp_http_client_handle_t client = esp_http_client_init(&config);
    if (client == NULL)
    {
        return InternalError;
    }
    ZWError err = __download_file_sub(ctx_download, zway, url, client, stream, ctx_stream);
    (void)esp_http_client_close(client);
    return err;
}

static ZWError __httpd_handler_cmd_parser_body(httpd_req_t *const req, __Esp32HandlerCtxDownload_t *const ctx_download, const ZWay zway)
{
    size_t content_len = req->content_len;
    while (content_len != 0)
    {
        size_t step = sizeof(ctx_download->buffer);
        if (step > content_len)
        {
            step = content_len;
        }
        const ssize_t received = httpd_req_recv(req, ctx_download->buffer, step);
        if (received <= 0)
        {
            zway_log_error(zway, Error, "httpd_req_recv - err", BadData);
            return __zwaysrv_upgrade_deinit_all_err(ctx_download, req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, InternalError);
        }
        const ZWError err = _zserialize_download_parser(&ctx_download->ctx_serialize, ctx_download->buffer, received);
        if (err != NoError)
        {
            return __zwaysrv_upgrade_deinit_all_err(ctx_download, req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
        }
        content_len = content_len - received;
    }
    return NoError;
}

static ZWError __httpd_handler_cmd_parser_upgrade(httpd_req_t *const req, __Esp32HandlerCtxDownload_t *const ctx_download, const ZWSTR uri, const ZWay zway)
{
    ctx_download->type = __Esp32HandlerCtxDownloadTypeUpdate;
    
    ZWError err = _zserialize_zway_upgrade_extract_node_id(zway, uri, &ctx_download->update.node_id);
    if (err != NoError)
    {
        return __httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
    }
    err = _zstream_storage_update_rw_init(&ctx_download->update.ctx_storage, _zddx_zway_operations_get_ctx(zway), FALSE);
    if (err != NoError)
    {
        return __httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
    }
    
    err = _zserialize_download_init(&ctx_download->ctx_serialize, req, __get_hdr_value_str, _zstream_storage_update_rw(), &ctx_download->update.ctx_storage);
    if (err != NoError)
    {
        _zstream_storage_update_rw_free(&ctx_download->update.ctx_storage);
        return __httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
    }
    
    err = __httpd_handler_cmd_parser_body(req, ctx_download, zway);
    if (err != NoError)
    {
        return err;
    }
    
    const ssize_t targetId = _zserialize_download_extract_target_id(&ctx_download->ctx_serialize);
    if (targetId < 0 || targetId > 255)
    {
        return __zwaysrv_upgrade_deinit_all_err(ctx_download, req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
    }
    
    if (_zserialize_download_is_file(&ctx_download->ctx_serialize))
    {
        err = _zstream_storage_update_rw_end(&ctx_download->update.ctx_storage, &ctx_download->update.result);
        if (err != NoError)
        {
            return __zwaysrv_upgrade_deinit_all_err(ctx_download, req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
        }
        __zwaysrv_upgrade_deinit_all(ctx_download);
        err = _zway_cc_firmware_update_perform_srv(&ctx_download->update.result, zway, ctx_download->update.node_id, targetId);
        if (err != NoError)
        {
            return __httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
        }
        return ___httpd_handler_cmd_parser_done(req);
    }
    
    {
        const ZWSTR url = _zserialize_download_extract_url(&ctx_download->ctx_serialize);
        if (url != NULL)
        {
            err = __download_file(ctx_download, zway, url, _zstream_storage_update_rw(), &ctx_download->update.ctx_storage);
            zfree(url);
            if (err != NoError)
            {
                zway_log_error(zway, Error, "__download_file", err);
                return __zwaysrv_upgrade_deinit_all_err(ctx_download, req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
            }
            err = _zstream_storage_update_rw_end(&ctx_download->update.ctx_storage, &ctx_download->update.result);
            if (err != NoError)
            {
                return __zwaysrv_upgrade_deinit_all_err(ctx_download, req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
            }
            __zwaysrv_upgrade_deinit_all(ctx_download);
            err = _zway_cc_firmware_update_perform_srv(&ctx_download->update.result, zway, ctx_download->update.node_id, targetId);
            if (err != NoError)
            {
                return __httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
            }
            return ___httpd_handler_cmd_parser_done(req);
        }
    }
    
    return __zwaysrv_upgrade_deinit_all_err(ctx_download, req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, InternalError);
}

static ZWError __httpd_handler_cmd_parser_restore(httpd_req_t *const req, __Esp32HandlerCtxDownload_t *const ctx_download, const ZWay zway)
{
    ctx_download->type = __Esp32HandlerCtxDownloadTypeRestore;
    
    ZWError err = _zstream_storage_restore_rw_init(&ctx_download->restore.ctx_storage, _zddx_zway_operations_get_ctx(zway), FALSE);
    if (err != NoError)
    {
        return __httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
    }
    err = _zserialize_download_init(&ctx_download->ctx_serialize, req, __get_hdr_value_str, _zstream_storage_restore_rw(&ctx_download->restore.ctx_storage), _zstream_storage_update_restore_rw_ctx(&ctx_download->restore.ctx_storage));
    if (err != NoError)
    {
        _zstream_storage_restore_rw_free(&ctx_download->restore.ctx_storage);
        return __httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
    }
    
    err = __httpd_handler_cmd_parser_body(req, ctx_download, zway);
    if (err != NoError)
    {
        return err;
    }
    
    if (_zserialize_download_is_file(&ctx_download->ctx_serialize))
    {
        err = _zstream_storage_restore_rw_end(&ctx_download->restore.ctx_storage, &ctx_download->restore.result);
        if (err != NoError)
        {
            return __zwaysrv_upgrade_deinit_all_err(ctx_download, req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
        }
        __zwaysrv_upgrade_deinit_all(ctx_download);
        err = _zway_controller_config_restore_stream(zway, ctx_download->restore.force, _zstream_storage_restore_reader(&ctx_download->restore.result), _zstream_storage_restore_reader_ctx(&ctx_download->restore.result));
        _zstream_storage_restore_reader_free(&ctx_download->restore.result);
        if (err != NoError)
        {
            return __httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
        }
        return ___httpd_handler_cmd_parser_done(req);
    }
    
    return __zwaysrv_upgrade_deinit_all_err(ctx_download, req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, InternalError);
}

static ZWError __httpd_handler_cmd_parser(httpd_req_t *const req, _ZwayEspWebServerData_t *const serv_data, __Esp32HandlerCtx_t *const ctx_httpd_handler, const ZWSTR uri, const ZWBOOL gzip)
{
    if (_zserialize_zway_is_command_upgrade(uri))
    {

        const ZWError err = __httpd_handler_cmd_parser_upgrade(req, &ctx_httpd_handler->common.download, uri, serv_data->zway);
        if (err != NoError)
        {
            zway_log_error(serv_data->zway, Error, "parse cmd: upgrade finware", err);
        }
        return err;
    }
    
    if (_zserialize_zway_is_command_restore(uri, &ctx_httpd_handler->common.download.restore.force))
    {
        const ZWError err = __httpd_handler_cmd_parser_restore(req, &ctx_httpd_handler->common.download, serv_data->zway);
        if (err != NoError)
        {
            zway_log_error(serv_data->zway, Error, "parse cmd: restore finware", err);
        }
        return err;
    }
    
    // It's an embedded API
    if (_zserialize_zway_parse_command_match(uri, "^/ZWave/ESP/(.+)$", ctx_httpd_handler->common.cgi.m))
    {
        return __httpd_handler_cmd_parser_embed(req, serv_data, ctx_httpd_handler, &uri[ctx_httpd_handler->common.cgi.m[1].rm_so]);
    }
    
    _zstream_http_write_esp_init(&ctx_httpd_handler->common.cgi.ctx_stream, _zway_get_logger(serv_data->zway), req, ctx_httpd_handler->common.cgi.buffer, sizeof(ctx_httpd_handler->common.cgi.ctx_stream));
    ZWError err = _zserialize_zway_parse(serv_data->zway, uri, _zstream_http_write_esp(), &ctx_httpd_handler->common.cgi.ctx_stream, gzip);
    if (err == NoError)
    {
        err = _zstream_http_write_esp_end(&ctx_httpd_handler->common.cgi.ctx_stream);
    }
    _zstream_http_write_esp_free(&ctx_httpd_handler->common.cgi.ctx_stream);
    if (err == NoError)
    {
        return NoError;
    }
    return __httpd_resp_send_err(req, (err == NotSupported) ? HTTPD_404_NOT_FOUND: HTTPD_500_INTERNAL_SERVER_ERROR, NULL, err);
}

static ZWBOOL __httpd_handler_tar_get_path_from_uri(ZWSTR dest, ZWCSTR base_path, ZWCSTR uri, size_t destsize)
{
    ZWCSTR find;
    size_t base_pathlen = strlen(base_path);
    size_t pathlen = strlen(uri);

    if ((find = strchr(uri, '?')) != NULL)
    {
        pathlen = MIN(pathlen, find - uri);
    }

    if ((find = strchr(uri, '#')) != NULL)
    {
        pathlen = MIN(pathlen, find - uri);
    }

    if (base_pathlen + pathlen + 1 > destsize)
    {
        /* Full path string won't fit into destination buffer */
        return FALSE;
    }

    /* Construct full path (base + path) */
    (void)strcpy(dest, base_path);
    (void)strlcpy(dest + base_pathlen, uri, pathlen + 1);
    /* Return pointer to path, skipping the base */
    return TRUE;
}

static ZWCSTR __httpd_handler_tar_set_content_type_from_file(ZWCSTR filename)
{
    static const ZWCHAR content_type_default[] = "text/plain"; /* For any other type always set as plain text */
    ZWCSTR mime_type;

    if ((mime_type = _zfile_get_mime_type(filename)) == NULL)
    {
        return content_type_default;
    }
    return mime_type;
}

static ZWBOOL __httpd_handler_tar_redirect_index(const httpd_req_t *req, size_t length)
{
    if (length == 0)
    {
        return TRUE;
    }
    if (req->uri[0] != '/')
    {
        return FALSE;
    }
    if (length == 1)
    {
        return TRUE;
    }
    length--;
    if (length < __web_arch_name_length || length > (__web_arch_name_length + 1))
    {
        return FALSE;
    }
    if (strncmp(&req->uri[1], __web_arch_name, __web_arch_name_length) != 0)
    {
        return FALSE;
    }
    if (length == __web_arch_name_length)
    {
        return TRUE;
    }
    if (req->uri[1 + __web_arch_name_length] == '/')
    {
        return TRUE;
    }
    return FALSE;
}

static ZWBOOL __httpd_handler_tar_redirect_files_sub(const httpd_req_t *const req, const size_t length, ZWCSTR const name, const size_t length_name)
{
    if (length == 0)
    {
        return FALSE;
    }
    if (req->uri[0] != '/')
    {
        return FALSE;
    }
    if ((length - 1) < (length_name + 1))
    {
        return FALSE;
    }
    if (strncmp(&req->uri[1], name, length_name) != 0)
    {
        return FALSE;
    }
    if (req->uri[1 + length_name] == '/')
    {
        return TRUE;
    }
    return FALSE;
}

static ZWBOOL __httpd_handler_tar_redirect_files(const httpd_req_t *req, size_t length)
{
    static ZWCSTR const names[] =
        {
            __web_arch_name,
            __web_arch_translations_name,
        };
    size_t i = 0;
    while (i < (sizeof(names) / sizeof(names[0])))
    {
        if (__httpd_handler_tar_redirect_files_sub(req, length, names[i], strlen(names[i])))
        {
            return TRUE;
        }
        i++;
    }
    return FALSE;
}

extern const char g_wui_tar_last_modified[];
static ZWError __httpd_handler_set_header_tar(httpd_req_t *const req, const ZWBOOL compressed)
{
    if (httpd_resp_set_hdr(req, "Last-Modified", g_wui_tar_last_modified) != ESP_OK)
    {
        return InternalError;
    }
    if (httpd_resp_set_hdr(req, "Cache-Control", "max-age=" ZWAY_ESP32_WEB_MAX_AGE) != ESP_OK)
    {
        return InternalError;
    }
    if (compressed)
    {
        if (httpd_resp_set_hdr(req, "Content-Encoding", "gzip") != ESP_OK)
        {
            return InternalError;
        }
    }
    return NoError;
}

static ZWError __httpd_handler_tar(httpd_req_t *req, _ZwayEspWebServerData_t *serv_data, __Esp32HandlerCtx_t *ctx_httpd_handler)
{
    extern const ZWBYTE wui_tar_start[] asm("_binary_wui_tar_start");
    extern const ZWBYTE wui_tar_end[] asm("_binary_wui_tar_end");
    ZWError err;
    ZWCSTR filepath;


    if (httpd_req_get_hdr_value_str(req, "If-Modified-Since", ctx_httpd_handler->common.tar.buffer, sizeof(ctx_httpd_handler->common.tar.buffer)) == ESP_OK)
    {
        if (strcmp(ctx_httpd_handler->common.tar.buffer, g_wui_tar_last_modified) == 0)
        {
            if (httpd_resp_send_custom_err(req, "304 Not Modified", "") == ESP_OK)
            {
                return NoError;
            }
        }
    }
    size_t length = strlen(req->uri);
    if (__httpd_handler_tar_redirect_index(req, length))
    {
        filepath = __web_arch_name_index_html;
    }
    else if (__httpd_handler_tar_redirect_files(req, length))
    {
        filepath = &req->uri[1];
    }
    else
    {
        if (__httpd_handler_tar_get_path_from_uri(ctx_httpd_handler->common.tar.buffer, __web_arch_name, req->uri, sizeof(ctx_httpd_handler->common.tar.buffer)) == FALSE)
        {
            return __httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL, InvalidArg);
        }
        filepath = ctx_httpd_handler->common.tar.buffer;
    }
    // zway_log(serv_data->zway, Debug, ZSTR("TAR: filename %s"), filepath);

    if ((err = _zfile_tar_data_find_file(zway_get_logger(serv_data->zway), wui_tar_start, (wui_tar_end - wui_tar_start), filepath, &ctx_httpd_handler->common.tar.tar_data_output)) != NoError)
    {
        return __httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist", err);
    }

    err = __httpd_handler_set_header_tar(req, ctx_httpd_handler->common.tar.tar_data_output.compressed);
    if (err != NoError)
    {
        zway_log(serv_data->zway, Error, ZSTR("Can't set HTTP headers"));
        return err;
    }
    if (httpd_resp_set_type(req, __httpd_handler_tar_set_content_type_from_file(filepath)) != ESP_OK)
    {
        return BadData;
    }
    if (httpd_resp_send(req, ctx_httpd_handler->common.tar.tar_data_output.file_data, ctx_httpd_handler->common.tar.tar_data_output.file_size) != ESP_OK)
    {
        return BadData;
    }
    return NoError;
}

static ZWError __httpd_handler_set_header(httpd_req_t *const req)
{
    const ZWCSTR allow_methods = "GET, PUT, POST, DELETE, OPTIONS";
    const ZWCSTR allow_headers = "Accept-Ranges, Content-Encoding, Content-Length, Content-Range, Content-Type, Date, Cache-Control, Content-Language, Accept-Language";
    esp_err_t status = httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
    if (status != ESP_OK)
    {
        return InternalError;
    }
    status = httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", allow_methods);
    if (status != ESP_OK)
    {
        return InternalError;
    }
    status = httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", allow_headers);
    if (status != ESP_OK)
    {
        return InternalError;
    }
    status = httpd_resp_set_hdr(req, "Access-Control-Expose-Headers", allow_headers);
    if (status != ESP_OK)
    {
        return InternalError;
    }
    return NoError;
}

static ZWError __httpd_handler_sub(httpd_req_t *const req, _ZwayEspWebServerData_t *const serv_data, __Esp32HandlerCtx_t *const ctx_httpd_handler, const ZWBOOL gzip)
{
    ZWSTR uri;
    ZWError err;
   
    err = __httpd_handler_set_header(req);
    if (err != NoError)
    {
        zway_log(serv_data->zway, Error, ZSTR("Can't set HTTP headers"));
        return err;
    }

    if (_zserialize_zway_command_contains(req->uri) == TRUE)
    {
        if ((err = _zurl_decode(zway_get_logger(serv_data->zway), req->uri, &uri)) != NoError)
        {
            return err;
        }
        err = __httpd_handler_cmd_parser(req, serv_data, ctx_httpd_handler, uri, gzip);
        zfree(uri);
        return err;
    }
    if (req->method == HTTP_GET)
    {
        if (!gzip)
        {
            return __httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, NULL, NotSupported);
        }
        err = __httpd_handler_tar(req, serv_data, ctx_httpd_handler);
        return err;
    }
    return __httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, NULL, NotSupported);
}

static esp_err_t __httpd_handler(httpd_req_t *const req)
{
    _ZwayEspWebServerData_t *const serv_data = (_ZwayEspWebServerData_t *)httpd_get_global_user_ctx(req->handle);
    zway_log(serv_data->zway, Debug, ZSTR("HTTP: Request %s"), req->uri);
    
    __Esp32HandlerCtx_t *const ctx_httpd_handler = (__Esp32HandlerCtx_t *)zassert(zmalloc(sizeof(ctx_httpd_handler[0])));
    if (ctx_httpd_handler == NULL)
    {
        return (ESP_OK);
    }
    const esp_err_t ret = httpd_req_get_hdr_value_str(req, "Accept-Encoding", ctx_httpd_handler->common.accept_encoding, sizeof(ctx_httpd_handler->common.accept_encoding));
    const ZWBOOL gzip = (ret != ESP_OK || strstr(ctx_httpd_handler->common.accept_encoding, "gzip") == NULL) ? FALSE : TRUE;
    
    (void)__httpd_handler_sub(req, serv_data, ctx_httpd_handler, gzip);
    zfree(ctx_httpd_handler);
    return (ESP_OK);
}

typedef union ZwayStartWebServerInit_s
{
    httpd_config_t config;
    httpd_uri_t config_uri;
} ZwayStartWebServerInit_t;

typedef struct ZwayStartWebServer_s
{
    ZwayStartWebServerInit_t init;
} ZwayStartWebServer_t;

static void __global_user_ctx_free_fn(void *ctx)
{
    zfree(ctx);
}

static bool __httpd_uri_match_wildcard(const char *template, const char *uri, size_t len)
{
    return true;
    (void)template;
    (void)uri;
    (void)len;
}

static ZWError __zway_esp32_web_server_start(_ZwayEsp32WebServerCtx_t *ctx, _ZwayEspWebServerData_t *server_data, ZwayStartWebServer_t *ctx_server)
{
    static const httpd_config_t config_default = HTTPD_DEFAULT_CONFIG();

    ctx_server->init.config = config_default;
    ctx_server->init.config.stack_size = ZWAY_ESP32_WEB_SERVER_STACK;
    ctx_server->init.config.core_id = 0;
    ctx_server->init.config.server_port = ZWAY_ESP_WEB_SERVER_PORT;
    ctx_server->init.config.global_user_ctx = server_data;
    ctx_server->init.config.global_user_ctx_free_fn = __global_user_ctx_free_fn;
    ctx_server->init.config.uri_match_fn = __httpd_uri_match_wildcard;
    if (httpd_start(&ctx->server, &ctx_server->init.config) != ESP_OK)
    {
        return InternalError;
    }

    /* URI handler for deleting files from server */
    (void)memset(&ctx_server->init.config_uri, 0, sizeof(ctx_server->init.config_uri));
    ctx_server->init.config_uri.uri = ""; // Match all URIs
    ctx_server->init.config_uri.method = HTTP_ANY;
    ctx_server->init.config_uri.handler = __httpd_handler;
    if (httpd_register_uri_handler(ctx->server, &ctx_server->init.config_uri) != ESP_OK)
    {
        (void)httpd_stop(ctx->server);
        return InternalError;
    }
    return NoError;
}

ZWError _zway_esp32_web_server_stop(_ZwayEsp32WebServerCtx_t *ctx)
{
    if (httpd_unregister_uri_handler(ctx->server, "", HTTP_ANY) != ESP_OK)
    {
        return InternalError;
    }
    if (httpd_stop(ctx->server) != ESP_OK)
    {
        return InternalError;
    }
    return NoError;
}

ZWError _zway_esp32_web_server_start(_ZwayEsp32WebServerCtx_t *ctx, ZWay zway)
{
    _ZwayEspWebServerData_t *server_data;
    ZwayStartWebServer_t *ctx_server;
    ZWError err;

    if ((ctx_server = zmalloc(sizeof(ZwayStartWebServer_t))) == NULL)
    {
        return BadAllocation;
    }
    if ((server_data = zmalloc(sizeof(_ZwayEspWebServerData_t))) == NULL)
    {
        zfree(ctx_server);
        return BadAllocation;
    }
    server_data->zway = zway;
    if ((err = __zway_esp32_web_server_start(ctx, server_data, ctx_server)) != NoError)
    {
        zfree(server_data);
    }
    zfree(ctx_server);
    return err;
}